package de.bloxel.world;

import static de.bloxel.core.BloxelFactory.DEFAULT_BLOXEL_DIMENSION;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.jme3.bounding.BoundingBox;
import com.jme3.math.Vector3f;

import de.bloxel.core.BloxelFactory.BloxelType;

/**
 * Hold a chunk (sector) of the world.
 * 
 * @author andreas
 * @since 0.1.0
 */
public class TerrainChunkImpl implements TerrainChunk {

  private static final Logger LOG = LoggerFactory.getLogger(TerrainChunkImpl.class);

  /**
   * List of all {@link TerrainElement}S of this chunk.
   */
  private final List<TerrainElement> terrainElements = Lists.newArrayList();

  /**
   * The origin position of the chunk, all {@link TerrainElement} are relative located to this point.
   */
  private final Vector3f center;

  /**
   * The dimension of the chunk.
   */
  private final BoundingBox boundingBox;

  /**
   * Will be used to add more {@link TerrainElement}S to the chunk.
   */
  private final TerrainLoader loader;

  /**
   * This implementation will call the loader during instance creation.
   * 
   * @param theCenter
   *          is absolute position in the "world", if you using only one chunk the use the {@link Vector3f#ZERO}, if you
   *          combine more than one chunk (i.e. in a grid of chunks for open world scenarios) then only the central
   *          chunk will have the {@link Vector3f#ZERO} as center and all other chunks will stay around the center chunk
   * @param aSize
   *          in x, y and z direction, then the chunk have a volume of (x:-aSize..aSize, y:-aSize..aSize,
   *          z:-aSize..aSize), depending on this size the chunk can have as much {@link TerrainElement}S as possible,
   *          all {@link TerrainElement}S of the chunk will be <b>inside</b> the chunk
   * @param theLoader
   *          will be used to add more {@link TerrainElement}S to the chunk
   */
  public TerrainChunkImpl(final Vector3f theCenter, final float aSize, final TerrainLoader theLoader) {
    center = theCenter;
    loader = theLoader;
    boundingBox = new BoundingBox(center, aSize, aSize, aSize);
    loader.fill(this);
  }

  @Override
  public void addTerrainElement(final TerrainElement aTerrainElement) {
    this.terrainElements.add(aTerrainElement);
  }

  @Override
  public void addTerrainElement(final Vector3f targetPosition, final BloxelType theBoxelType) {
    final TerrainElement te = new TerrainElement(targetPosition, Vector3f.UNIT_XYZ.mult(DEFAULT_BLOXEL_DIMENSION),
        theBoxelType);
    terrainElements.add(te);
    LOG.trace(String.format("Add terrain-element %s at position %s", te, targetPosition));
  }

  @Override
  public BoundingBox getBoundingBox() {
    return boundingBox;
  }

  @Override
  public Vector3f getCenter() {
    return center;
  }

  @Override
  public int getNumberOfElements() {
    return terrainElements.size();
  }

  @Override
  public List<TerrainElement> getTerrainElements() {
    return ImmutableList.<TerrainElement> builder().addAll(terrainElements).build();
  }

  @Override
  public List<TerrainElement> getTerrainElements(final Vector3f thePlayerLocation) {
    final List<TerrainElement> nodes = new ArrayList<TerrainElement>();
    // XXX hardcoded - should be a parameter!
    final Vector3f eventHorizon = Vector3f.UNIT_XYZ.mult(1024);
    // TODO die position des player muss noch normalisiert werden, d.h. auf "ganzahlige" vielfache gerundet werden
    final BoundingBox playerEventHorizon = new BoundingBox(thePlayerLocation, eventHorizon.x, eventHorizon.y,
        eventHorizon.z);
    for (final TerrainElement element : terrainElements) {
      if (playerEventHorizon.intersects(element.getCenter())) {
        nodes.add(element);
        continue;
      }
    }
    LOG.trace(String.format("Found (%d from %d) terrain-elements for player %s with horizon %s", nodes.size(),
        terrainElements.size(), thePlayerLocation, eventHorizon));
    return nodes;
  }

  @Override
  public List<TerrainElement> getTerrainElements(final Vector3f thePlayerLocation, final Vector3f thePlayerDirection) {
    // XXX hier waere es gut die direction irgendwie noch mit einzubeziehen oder?!
    return getTerrainElements(thePlayerLocation);
  }

  void removeTerrainElement(final TerrainElement theTerrainElement2Remove) {
    if (terrainElements.remove(theTerrainElement2Remove)) {
      LOG.trace(String.format("Removed terrain-element %s", theTerrainElement2Remove));
    } else {
      LOG.trace(String.format("Terrain-element %s was not removed", theTerrainElement2Remove));
    }
  }

  @Override
  public void removeTerrainElement(final Vector3f theCenterVector) {
    // TODO helper method findTerrainElementByCenterPosition
    for (final Iterator<TerrainElement> iterator = terrainElements.iterator(); iterator.hasNext();) {
      final TerrainElement te = iterator.next();
      if (te.getCenter().equals(theCenterVector) && te.isRemoveable()) {
        iterator.remove();
        LOG.trace(String.format("Removed terrain-element %s at position %s", te, theCenterVector));
        return;
      }
    }
    LOG.trace(String.format("No terrain-element at position %s", theCenterVector));
  }

  @Override
  public String toString() {
    return String.format("%s: bounding-box:%s, elements:%d", super.toString(), boundingBox, terrainElements.size());
  }
}